iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
Software Development

30天打造一套企業PLM系列 第 2

Day 2:Monorepo 與建置管線——前後端解耦開發、靜態資源打包,如何用一個 repo 管好兩個世界?

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260819/20161290FCjfjbG8gP.jpg

系列:30 天打造企業級 PLM|面向:全端|素材:前端打包設定、打包管線腳本

問題場景

昨天決定自建 PLM 之後,第一個工程決策不是選框架,是選「程式碼怎麼放」。前後端各開一個 repo 是很多團隊的直覺,但小團隊很快會嘗到苦頭:API 契約改了,另一個 repo 的人不知道;要重現三個月前的版本,得先考古「當時前端配哪一版後端」;兩條 CI/CD 管線,維護成本直接翻倍。Mini-PLM 的答案是一個 Monorepo、前後端解耦開發、單一產物打包(前端 build 產物直接注入後端 static 靜態資源目錄)、一支腳本走完全部流程。

https://ithelp.ithome.com.tw/upload/images/20260819/20161290x6M7m44xSV.jpg

商業邏輯設計

先講業務端的真實條件,因為它們直接決定了架構。內網部署、沒有專職 DevOps,部署流程必須簡單到「一個人、一份文件、照做就好」。使用尖峰集中在上班時段,全公司幾百人,單機其實撐得住(Day 21 壓測會證明這件事)。所以初期單機部署,架構上保留前後端各自橫向擴展的路。擴展是投資選項,不用第一天就付錢。

這是本系列第一個「商業取捨決定技術架構」的例子:問題從來不是能不能做分散式,是現在值不值得付分散式的複雜度。

技術選型與取捨

架構演進:從 WebLogic 重裝 EAR 到現代前後端解耦與靜態封裝

老 PLM 人應該都經歷過 J2EE / EJB 時代的建置噩夢。

以前 Oracle Agile 是打包成一個動輒幾百 MB 的巨大 Enterprise Archive (.ear) 檔部署到 WebLogic Server。這個 EAR 裡面塞滿了 EJB-JAR(後端邏輯與 Entity Beans)、WAR(JSP / Servlet Web Client)、Applet / Swing 相關 JAR(Java Client 管理介面),以及上百個依賴套件。那時最折磨人的就是 WebLogic 的 ClassLoader 階層隔離問題——不同模組依賴相同套件的不同版本,稍微配置不慎就會觸發 ClassNotFoundExceptionLinkageError,只能在 weblogic-application.xml 裡跟 <prefer-application-packages> 痛苦搏鬥;改行代碼重新部署 EAR,WebLogic 往往要卡上好幾分鐘。

Mini-PLM 採用現代前後端解耦、單一產物交付的輕量架構:

Mini-PLM/
├── backend/     # Spring Boot(Maven)
├── frontend/    # React 19 + Vite(npm)
├── scripts/     # 打包、部署、seed 等 PowerShell 腳本
├── e2e/         # Playwright 端對端測試
└── docs/        # 規格與文件

Monorepo,但前後端徹底解耦

關鍵是同一個 repo、不同的建置系統:backend 用 Maven、frontend 用 npm,彼此不依賴對方的建置工具。耦合點只有兩個:API 契約(JSON over HTTP)和打包腳本。一次 commit 就是一份「前後端保證相容」的快照,回溯任何歷史版本都不用考古前端當時配哪一版後端。

單一產物交付:前端靜態資源封裝進後端

前端 vite build 的產物是純靜態檔案(HTML/JS/CSS),在打包階段由腳本複製到後端的 src/main/resources/static,與 Spring Boot 編譯產物一同封裝成單一的 WAR/JAR 檔。相較於以前 WebLogic 複雜的多層 EAR 與 ClassLoader 衝突,這種「單一交付物(Single Artifact)」架構兼具前後端獨立開發的敏捷性與單機極簡維運的優勢:伺服器啟動即同時託管前端靜態資源與 /api REST 端點,零跨網域(CORS)困擾,上線只需部署一個檔案。

dev / prod 的 base path 切換:解決 Tomcat 伺服器的 Context Path 問題

在企業環境中,WAR 檔通常是部署到現有的 獨立 Tomcat 伺服器(放置於 webapps/ 目錄下)。

此時 Tomcat 會自動根據 WAR 檔名或配置將應用程式掛載在特定的 Context Path(例如 /miniplm//rdims2/),這與本機 Vite 開發伺服器跑在根目錄 /http://localhost:5173/)截然不同。

如果沒有處理這個路徑位移,前端打包產出的 HTML 會直接向 /assets/index-xxx.js 抓取資源,瀏覽器在 Tomcat 上就會全部噴出 404 Not Found 白畫面。vite.config.ts 用一行動態切換解決這個部署痛點:

// production 部署於 Tomcat Context Path(/miniplm/);dev 為根路徑
base: mode === 'production' ? '/miniplm/' : '/',

看似平凡的一行設定,卻是很多「本機開發正常、上線到 Tomcat 全部 404」問題的總開關。所有的 JS/CSS 靜態資產引用、前端 React Router 的 basename、以及 API 呼叫前綴,都必須與 Tomcat 的 Context Path 保持一致。

核心內容:一支腳本走完打包

https://ithelp.ithome.com.tw/upload/images/20260819/20161290MZ62v0iYt0.jpg

打包入口是 scripts/package-project.ps1,固定流程:產版本、前端 build、後端 compile、靜態檔就位、打包。重點不在步驟,在每一步後面的這個函式:

# 檢查上一個 native command(npm / mvnw 等)的 exit code,非 0 即中止
function Assert-NativeSuccess {
    param([Parameter(Mandatory = $true)][string]$Step)

    if ($LASTEXITCODE -ne 0) {
        throw "$Step 失敗 (exit code: $LASTEXITCODE)"
    }
}

為什麼需要它?因為 PowerShell 的 $ErrorActionPreference = "Stop" 對 native command 無效。npm 或 mvnw 失敗了,腳本照樣往下跑,結果是:

tsc 編譯失敗 → vite build 沒跑 → dist 是舊的 → 部署腳本把舊前端複製進去 → 打包「成功」→ 一個含舊程式碼的封裝檔靜靜地部到生產環境。

這就是 Day 1 預告過的打包管線 silent failure。它可怕的地方在於每一步看起來都是綠的。修法很無聊(檢查 exit code),教訓很貴:管線裡每一個「不會失敗的步驟」,都要先問一句,它失敗的時候我知道嗎?

踩坑記錄:vendor chunk 拆細,生產環境白畫面

這是全系列排行榜前三名的坑。起因很合理:想把 node_modules 按套件拆成 vendor-reactvendor-antdvendor-data 幾包,讓快取更細緻。本機 dev server 一切正常,部署到生產,使用者打開只有一片白。

原因是套件之間互相依賴,細拆會產生 chunk 循環引用,載入順序無法保證。實際案發現場是 vendor-data 在 React 初始化之前就執行了 createContext。而 dev 模式根本不打包,所以你在開發環境永遠看不到這個問題。

https://ithelp.ithome.com.tw/upload/images/20260819/201612908DTvbsf7yk.jpg

最終版的 manualChunks 連同血淚都寫進註解裡:

/**
 * 將 node_modules 依賴全部歸入單一 vendor chunk。
 * 目的:業務碼與第三方分離,業務碼改版不必重下 vendor(長期快取)。
 * 注意:不可按套件再細拆(react/antd/data 各一包)——套件間互相依賴會產生
 * chunk 循環引用,載入順序無法保證,曾造成生產 build 白畫面
 * (vendor-data 在 React 初始化前執行 createContext)。dev 模式不打包
 * 看不出來,改動此函式後必須以 e2e/scripts/verify-dist-build.mjs 驗證生產 build。
 */
function manualChunks(id: string): string | undefined {
  if (!id.includes('node_modules')) {
    return undefined
  }
  return 'vendor'
}

將第三方套件統一收進單一 vendor chunk,既實現了「業務碼更新時、第三方依賴長期快取」的目的,又徹底杜絕了細拆所引發的循環引用風險。

證明的方式也留下來了。一支 20 行的驗證腳本,用 headless browser 打開生產 build(不是 dev server),確認 React 真的掛載、console 沒有錯誤:

// 驗證前端「生產 build」可正常啟動渲染(dev server 不打包,抓不到 chunk 循環問題)
await page.goto(URL, { waitUntil: 'networkidle' });
const rootChildren = await page.evaluate(
  () => document.getElementById('root')?.children.length ?? 0);

if (errors.length > 0 || rootChildren === 0) {
  console.log(`FAIL — root children: ${rootChildren}`);
  process.exit(1);
}

從此改 chunking 只有一條規矩:改完必跑 verify-dist-build.mjs

小結

小團隊的 Monorepo 其實是「版本相容性」最便宜的解法,一次 commit 就是一份前後端相容快照。前端解耦開發、建置時注入後端靜態資源形成單一產物,兼顧了開發效率與極簡部署。至於建置管線,最危險的從來不是會失敗的步驟,是失敗了你不知道的步驟。

明日 Day 3:PLM 領域模型怎麼設計——品項、版本、結構、表單四大聚合,一張概念圖講完。


上一篇
Day 1:為什麼要自己打造企業 PLM?
下一篇
Day 3:PLM 領域模型怎麼設計
系列文
30天打造一套企業PLM4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言